package de.recipeminer.persistence;

import com.db4o.ObjectSet;
import com.db4o.query.Query;
import com.google.common.base.Objects;
import org.apache.log4j.Logger;

/**
 * Defines operations for CRUD-operations in a {@link Db4oContainer} referring to a specific, associated instance
 * of a given class {@code T}. For the lifetime of a {@code Db4oObjectCompanion} there should a 1:1 relationship
 * between the companion instance and {@link Db4oObjectCompanion#associatedObject}.
 * To enable instance of a class to use a {@code Db4oObjectCompanion}, that class should implement
 * {@link Db4oCompanionProvider}.
 * The main task of associatedObject companions is to help writers of client code to ensure that no accidental duplicates of
 * objects are created in the database if they are already saved there.
 * 
 * 
 * @author Markus Ackermann
 */
public class Db4oObjectCompanion<T extends Db4oCompanionProvider<T>> {
    private static Logger logger = Logger.getLogger(Db4oObjectCompanion.class);
    private T associatedObject;
    private Db4oContainer associatedContainer;


    /**
     * Creates an associatedObject companion with {@link Db4oObjectCompanion#associatedObject}
     * as associated associated object. This
     * constructor should usually only be used by a {@link Db4oCompanionFactory}.
     * 
     * @param associatedObject the object this compation will provide database operations for
     * @param associatedContainer ensures the same container is used for the lifetime of this compation
     */
    protected Db4oObjectCompanion(T associatedObject, Db4oContainer associatedContainer) {
        this.associatedObject = associatedObject;
        this.associatedContainer = associatedContainer;
    }

    
    /**
     * Returns a set of all objects saved in the currently active database that are identical
     * with the associated associatedObject according to the defined {@code db4oEquals} for class {@code T}
     *
     * @return associatedObject set of identical objects (as defined by the class of
     *         {@link Db4oObjectCompanion#associatedObject}
     */
    public ObjectSet<T> getIndeticalObjects() {
        associatedObject.updateIdentifier();
        Query identQuery =  associatedContainer.query();
        identQuery.constrain(associatedObject.getClass()); // same class as associated associatedObject
        // contraint for: candidate associatedObject has same db4o identifiert value as associated associatedObject
        identQuery.descend(associatedObject.db4oIdentifierFieldName()).constrain(associatedObject.db4oIdentifier());
        return identQuery.execute();
    }

    /**
     * Checks whether an identical associatedObject (with respect to {@code db4oEquals}) exists
     * in the currently active database.
     *
     * @return true, if identical objects exists
     */
    public boolean similarObjectsPresentInDatabase() {
        return getIndeticalObjects().size() > 0;
    }

    /**
     * Replaces a unique identical associatedObject (with respect to {@code db4oEquals}) of the associated
     * associatedObject with the associated associatedObject.
     * If the associated associatedObject is already the unique identical associatedObject in the database, nothing
     * happens. If there is no identical instance in the database so far, the associated associatedObject
     * is stored in the database.
     *
     * @throws NonUniqueObjectCounterpartInDatabaseException if it is ambiguous which identical associatedObject
     *         should be replaced
     */
    public void replace() {
        checkIfValid();
        associatedObject.updateIdentifier();
        ObjectSet<T> identicalObjects = getIndeticalObjects();

        if ( ! identicalObjects.isEmpty()) {
            if(identicalObjects.size() > 1) {
                // we detected an illegal database state (several objects with same db4o identifier)
                throw new NonUniqueObjectCounterpartInDatabaseException(objectDescriptionForException());
            } else if (identicalObjects.get(0) != associatedObject) { //maybe use equals() instead?
                // delete current counterpart object in the database (to replace it)
                associatedContainer.delete(identicalObjects.get(0));
            } else {
                //nothing to do
                associatedContainer.store(associatedObject);
            }
        }
        // it is ensured that no db4o-identical object is in the database - we can safely store
        associatedContainer.store(associatedObject);
    }

    /**
     * Saves the associated associatedObject in the database, if no identical associatedObject of the associated associatedObject
     * is present in the database. Fails if there is already an db4o-identical associatedObject present in the database
     * that is not identical the associated associatedObject (using {@code ==}).
     * If the associated associatedObject is already the unique identical associatedObject in the database, it is updated in the
     * database.
     *
     * @throws ObjectCounterpartAlreadyInDatabaseException if this operation would add another identical instance
     * @throws NonUniqueObjectCounterpartInDatabaseException if there is more than ohne (db4o-)identical associatedObject in
     *         the database
     * 
     */
    public void save() {
        checkIfValid();
        associatedObject.updateIdentifier();
        ObjectSet<T> identicalObjects = getIndeticalObjects();
        
        if ( ! identicalObjects.isEmpty()) {
            if(identicalObjects.size() > 1) {
                // we detected an illegal database state (several objects with same db4o identifier)
                throw new NonUniqueObjectCounterpartInDatabaseException(objectDescriptionForException());
            } else if (identicalObjects.get(0) != associatedObject) {
                // associated object is the unique counterpart object in the db4o, we can safely store
                throw new ObjectCounterpartAlreadyInDatabaseException(objectDescriptionForException());
            }
        }
        // either no db4o-identical objects in the database or associated object is a uniuq counterpart.
        // we can now safely store the associated object in both cases
        associatedContainer.store(associatedObject);
    }

    /**
     * Stores the associated object without any checks to prevent object dublicates with identical
     * db4o-identifier. If it has been ensured externally that the associated object is either
     * has do identical counterpart in the database or that the associated object is an object loaded
     * by the associated object container, this method can be used to increase performance by evading
     * the overhead for checking these conditions.
     */
    public void uncheckedSave() {
        checkIfValid();
        associatedObject.updateIdentifier();
        associatedContainer.store(associatedObject);
    }
    
    
    private void checkIfValid() {
        if (!associatedObject.validate()) {
            String msg = String.format("Persistence error: Validation failed for associatedObject %s (%s) " +
                    "while attempting to save it.", associatedObject, associatedObject.getClass());
            throw new IllegalStateException(msg);
        }
    }

    public static class NonUniqueObjectCounterpartInDatabaseException extends IllegalStateException {
        public NonUniqueObjectCounterpartInDatabaseException(String s) {
            super(s);
        }
    }

    public static class ObjectCounterpartAlreadyInDatabaseException extends IllegalStateException {
        public ObjectCounterpartAlreadyInDatabaseException(String s) {
            super(s);
        }
    }

    private String objectDescriptionForException() {
        return Objects.toStringHelper(associatedObject.getClass())
                   .add("db4o-identity value:", associatedObject.db4oIdentifier()).toString();
    }
}
